package mark.wxming.twotigerlock.lock.redis;

import mark.wxming.twotigerlock.lock.ILockHandler;
import mark.wxming.twotigerlock.lock.ILockIdentify;
import mark.wxming.twotigerlock.utils.Redis;
import org.apache.commons.collections.CollectionUtils;
import org.apache.log4j.Logger;
import redis.clients.jedis.Jedis;
import redis.clients.jedis.Pipeline;
import redis.clients.jedis.exceptions.JedisConnectionException;

import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.TimeUnit;

/**
 * 参考https://blog.csdn.net/java2000_wl/article/details/8740911 改造。
 * redis 全局锁实现
 * @author twotiger-wxm.
 * @date 2019/4/23.
 */
public class RedisLockHandler implements ILockHandler {

    private static final Logger LOGGER = Logger.getLogger(RedisLockHandler.class);

    /**
     * 单个锁的占用时间最长时30s
     */
    private static final int DEFAULT_SINGLE_EXPIRE_TIME = 30;

    /**
     * 批量锁最长占用 60s
     */
    private static final int DEFAULT_BATCH_EXPIRE_TIME = 60;

    /**
     * redis 库
     */
    private int database = 1;

    /**
     *
     */
    public RedisLockHandler(int database) {
        this.database = database;
    }

    public boolean tryLock(ILockIdentify iLockIdentify) {
        return tryLock(iLockIdentify, 0L, null);
    }

    public boolean tryLock(ILockIdentify iLockIdentify, long timeout, TimeUnit unit) {
        return tryLock(iLockIdentify, timeout, unit, true);
    }

    private boolean tryLock(ILockIdentify iLockIdentify, long timeout, TimeUnit unit, boolean retry) {
        String key = (String) iLockIdentify.uniqueIdentify();
        Jedis jedis = null;
        try {
            jedis = getResource();
            //System有一个静态的函数nanoTime函数，该函数是返回纳秒的。1毫秒=1纳秒*1000*1000。该函数只能用于计算时间差。
            // https://blog.csdn.net/itwxming/article/details/89491817
            long nano = System.nanoTime();
            do {
                LOGGER.debug("try lock key: " + key);
                Long i = jedis.setnx(key, System.currentTimeMillis() + "");
                if (i == 1) {
                    jedis.expire(key, DEFAULT_SINGLE_EXPIRE_TIME);
                    LOGGER.debug("get lock, key: " + key + " , expire in "
                            + DEFAULT_SINGLE_EXPIRE_TIME + " seconds.");
                    return Boolean.TRUE;
                } else { // 存在锁
                    if (LOGGER.isDebugEnabled()) {
                        String desc = jedis.get(key);
                        LOGGER.debug("key: " + key + " locked by another business：" + desc);
                    }
                }
                if (timeout == 0) {
                    break;
                }
                Thread.sleep(300);
            } while ((System.nanoTime() - nano) < unit.toNanos(timeout));//https://blog.csdn.net/itwxming/article/details/89492187
            return Boolean.FALSE;
        } catch (JedisConnectionException e) {
            if (retry) {
                LOGGER.warn(e.getMessage(), e);
                LOGGER.info("1s后重试");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e1) {
                    LOGGER.error(e.getMessage(), e);
                }
                tryLock(iLockIdentify, timeout, unit, false);
            } else {
                LOGGER.error("重试失败" + e.getMessage(), e);
                jedis.close();
            }
        } catch (Exception e) {
            LOGGER.error(e.getMessage(), e);
        } finally {
            jedis.close();
        }
        return Boolean.FALSE;
    }

    public void lock(ILockIdentify iLockIdentify) {
        lock(iLockIdentify, true);
    }

    private void lock(ILockIdentify iLockIdentify, boolean retry) {
        String key = (String) iLockIdentify.uniqueIdentify();
        Jedis jedis = null;
        try {
            jedis = getResource();
            /**
             * 此处计时开始
             */
            long nano = System.nanoTime();
            boolean flag = true;
            do {
                LOGGER.debug("lock key: " + key);
                /* setnx的含义就是SET if Not Exists，
                setnx(key, value)设置 key对应的值为 string类型的 value。
                该方法是原子的，如果key不存在，则设置当前key成功，返回1；
                如果当前key已经存在，则设置当前key失败，返回0。
                 */
                Long i = jedis.setnx(key, System.currentTimeMillis() + "");
                if (i == 1) {
                    //在cil里执行命令亲测：expire设置的超时时间是基于当前设置时间的，而不是key写入时间。
                    jedis.expire(key, DEFAULT_SINGLE_EXPIRE_TIME);
                    LOGGER.debug("get lock, key: " + key + " , expire in "
                            + DEFAULT_SINGLE_EXPIRE_TIME + " seconds.");
                    return;
                } else {
                    if (LOGGER.isDebugEnabled()) {
                        String desc = jedis.get(key);
                        LOGGER.debug("key: " + key + " locked by another business：" + desc);
                    }
                }
                //应该再传过一个根据业务需要设定的超时时间，防止之前加锁没设定过期就挂掉，同样的请求再次过来后一直无法加锁在此死循环。
                // 超时便执行expire设定过期。
                // 实际直接用过期时间参数判断超时就行，正常那个过期时间长度该锁也应该失效了。
                //这个位置应该加代码：
                if(flag && (System.nanoTime() - nano)>TimeUnit.SECONDS.toNanos(DEFAULT_SINGLE_EXPIRE_TIME*3)){
                    jedis.expire(key, DEFAULT_SINGLE_EXPIRE_TIME);
                    flag=false;//鉴于之后时间条件一直满足，不能让一直更新设置过期时间，所以加个flag，此处代码只能走一次。
                }
                //因为先开始计时，后设置的超时时间，所以此处计时的结果最大值肯定可以大于超时时间。所以至少1倍就行。
                // 但防止中间操作redis时IO慢，或者内存卡执行慢，且该代码是防止极端情况下，实际很少用，所以该时间设置个大的，鉴于常量设了30秒，所以取3倍就差不多了。
                Thread.sleep(300);
            } while (true);
        } catch (JedisConnectionException e) {
            if (retry) {
                LOGGER.warn(e.getMessage(), e);
                LOGGER.info("1s后重试");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e1) {
                    LOGGER.error(e.getMessage(), e);
                }
                lock(iLockIdentify, false);
            } else {
                LOGGER.error("重试失败" + e.getMessage(), e);
                jedis.close();
            }
        } catch (Exception e) {
            LOGGER.error(e.getMessage(), e);
        } finally {
            jedis.close();
        }
    }

    public void unLock(ILockIdentify iLockIdentify) {
        List<ILockIdentify> list = new ArrayList<ILockIdentify>();
        list.add(iLockIdentify);
        unLock(list);
    }

    public boolean tryLock(List<ILockIdentify> iLockIdentifyList) {
        return tryLock(iLockIdentifyList, 0L, null);
    }

    public boolean tryLock(List<ILockIdentify> iLockIdentifyList, long timeout, TimeUnit unit) {
        Jedis jedis = null;
        try {
            List<String> needLocking = new CopyOnWriteArrayList<String>();
            List<String> locked = new CopyOnWriteArrayList<String>();
            jedis = getResource();
            long nano = System.nanoTime();
            do {
                // 构建pipeline，批量提交
                Pipeline pipeline = jedis.pipelined();
                for (ILockIdentify identify : iLockIdentifyList) {
                    String key = (String) identify.uniqueIdentify();
                    needLocking.add(key);
                    pipeline.setnx(key, System.currentTimeMillis() + "");
                }
                LOGGER.debug("try lock keys: " + needLocking);
                // 提交redis执行计数
                List<Object> results = pipeline.syncAndReturnAll();
                for (int i = 0; i < results.size(); ++i) {
                    Long result = (Long) results.get(i);
                    String key = needLocking.get(i);
                    if (result == 1) { // setnx成功，获得锁
                        jedis.expire(key, DEFAULT_BATCH_EXPIRE_TIME);
                        locked.add(key);
                    }
                }
                needLocking.removeAll(locked); // 已锁定资源去除

                if (CollectionUtils.isEmpty(needLocking)) {
                    return true;
                } else {
                    // 部分资源未能锁住
                    LOGGER.debug("keys: " + needLocking + " locked by another business：");
                }

                if (timeout == 0) {
                    break;
                }
                Thread.sleep(500);
            } while ((System.nanoTime() - nano) < unit.toNanos(timeout));

            // 得不到锁，释放锁定的部分对象，并返回失败
            if (!CollectionUtils.isEmpty(locked)) {
                jedis.del(locked.toArray(new String[0]));
            }
            return false;
        } catch (JedisConnectionException je) {
            LOGGER.error(je.getMessage(), je);
        } catch (Exception e) {
            LOGGER.error(e.getMessage(), e);
        } finally {
            jedis.close();
        }
        return true;
    }

    public void unLock(List<ILockIdentify> iLockIdentifyList) {
        List<String> keys = new CopyOnWriteArrayList<String>();
        for (ILockIdentify identify : iLockIdentifyList) {
            String key = (String) identify.uniqueIdentify();
            keys.add(key);
        }
        Jedis jedis = null;
        try {
            jedis = getResource();
            jedis.del(keys.toArray(new String[0]));
            LOGGER.debug("release lock, keys :" + keys);
        } catch (JedisConnectionException je) {
            LOGGER.error(je.getMessage(), je);
        } catch (Exception e) {
            LOGGER.error(e.getMessage(), e);
        } finally {
            jedis.close();
        }
    }

    private Jedis getResource() {
        return Redis.getResource(database);
    }

}
